﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Xml;
using CookieCollection = Business.Entities.CookieCollection;
using Cookie = Business.Entities.Cookie;
using System.Text;
using System.Diagnostics;
using Business.Entities;

namespace Business.Logic
{
    public class GoogleLogin
    {
        public string Email;
        public string Passwd;
        public Dictionary<string, string> KeyVal;
        private CookieCollection _cookies;
        public bool AllowWriteStreamBuffering;
        public bool IsGoogleAppsAccount;
        public string GoogleAppsDomain;

        private LoginErrorPackage _errorPackage;
        /// <summary>
        /// Debug data
        /// </summary>
        public LoginErrorPackage ErrorPackage
        {
            get { return _errorPackage; }
        }

        /// <summary>
        /// Cookies set after login
        /// </summary>
        public CookieCollection Cookies
        {
            get
            {
                return _cookies;
            }
        }

        /// <summary>
        /// Final URL set after login complete
        /// </summary>
        //public string TasksUrl = "http://mail.google.com/tasks/ig";

        //private const string GoogleLoginUrl = "https://www.google.com/accounts/ServiceLoginAuth?service=goanna_mobile";
        //private const string GoogleLoginUrl = "https://www.google.com/accounts/ServiceLoginAuth?service=goanna_mobile&followup=http://mail.google.com/tasks/ig&continue=http://mail.google.com/tasks/ig"
        private const string GoogleLoginFormUrl = "https://www.google.com/accounts/ServiceLogin?service=goanna_mobile&passive=true&nui=1&continue=https%3A%2F%2Fmail.google.com%2Ftasks%2Fig%3Fpli%3D1&followup=https%3A%2F%2Fmail.google.com%2Ftasks%2Fig%3Fpli%3D1";
        private const string GoogleLoginPostUrl = "https://www.google.com/accounts/ServiceLoginAuth?service=goanna_mobile";

        private const string AppsLoginFormUrl = "https://www.google.com/a/{0}/ServiceLogin?service=goanna_mobile&passive=true&nui=1&continue=https%3A%2F%2Fmail.google.com%3A443%2Ftasks%2Fa%2F{0}%2Fig";
        private const string AppsLoginPostUrl = "https://www.google.com/a/{0}/LoginAction2?service=goanna_mobile";
        
        private const string GoogleAuthCookie = "GTL";
        private const string AppsAuthCookie = "GTLHS";

        static GoogleLogin()
        {
            ServicePointManager.CertificatePolicy = new TrustAllCertificatePolicy();
            ServicePointManager.Expect100Continue = false;
        }

        public GoogleLogin()
        {
            _cookies = new CookieCollection();
            //_cookies.Add(new Cookie(CookieCollection.LocaleCookie, "en-AU"));
            _errorPackage = new LoginErrorPackage();
        }

        public GoogleLogin(bool isGoogleApps) : this()
        {
            IsGoogleAppsAccount = isGoogleApps;
        }

        public string GetPostData()
        {
            if (KeyVal == null) throw new ArgumentException("LoadForm must be run first");
            if (String.IsNullOrEmpty(Passwd) || String.IsNullOrEmpty(Email))
                throw new ArgumentException("Username and password not set");
            // Add uname/pwd
            KeyVal.Remove("Email");
            KeyVal.Remove("Passwd");
            KeyVal.Add("Email", Email);
            KeyVal.Add("Passwd", Passwd);
            // Make output string
            string o = "";
            int count = 0;
            foreach (KeyValuePair<string, string> elem in KeyVal)
            {
                o += elem.Key + "=" + StringUtil.UrlEncode(elem.Value);
                if (++count < KeyVal.Count) o += "&";
            }
            return o;
        }

        public enum LoginResult
        {
            Ok,
            WrongUsernameOrPass,
            TuringTest,
            Unknown
        }

        public LoginResult Login(string username, string password)
        {
            Email = username;
            Passwd = password;
            return Login();
        }

        private HttpLog _httpLog;

        /// <summary>
        /// Performs a login with the password and username set
        /// </summary>
        /// <returns></returns>
        public LoginResult Login()
        {
            _errorPackage.Reset();

            CHECK(200);
            LoadForm(); // Fetches load form and parses <input> tags
            CHECK(201);
            Trace.WriteLine("Logging in...");
            string postData = GetPostData(); // Prepares post data with login/pass data
            CHECK(202);

            // Find post url
            string postUrl = GoogleLoginPostUrl;
            if (IsGoogleAppsAccount) postUrl = String.Format(AppsLoginPostUrl, GoogleAppsDomain);

            // Prepare headers
            var headers = new WebHeaderCollection();
            if (_cookies.Count > 0) headers.Add("Cookie", _cookies.ToStringFor(postUrl));
            CHECK(203);

            // Log request to send
            _httpLog = _errorPackage.AddNewHttpLog(postUrl, _cookies, _errorPackage.LastCheckPoint);
            
            // Prepare request
            var rq = GoogleApi.GetPostReqest(postUrl, postData, headers);
            CHECK(204);
            LoginResult result = LoginResult.Unknown;
            string redirectUrl = "";
            // First post: ServiceLogin.aspx / LoginAction2 (apps)
            using (HttpWebResponse response = (HttpWebResponse) rq.GetResponse()) 
            {
                CHECK(205);
                result = ParseLoginResult(response);
                CHECK(206);
                if (result == LoginResult.Ok)
                {
                    redirectUrl = response.Headers["location"];
                    CHECK(207);
                    var newCookies = _cookies.SetCookiesFiltered(response);
                    _httpLog.SetHeadersOut(response);
                    _httpLog.CookiesOut = newCookies; // Log newly received cookies
                    _httpLog = _errorPackage.AddNewHttpLog(redirectUrl, _errorPackage.LastCheckPoint); // Prepare new log
                    CHECK(208);
                }
            }
            string nextUrl;
            if (result == LoginResult.Ok)
            {
                CHECK(209);
                bool locationRedirect = false;
                if (redirectUrl.Contains("TokenAuth")) redirectUrl = LoginPhase3(redirectUrl, true); // Additional step in Google Apps. 
                nextUrl = LoginPhase2(redirectUrl, out locationRedirect); //CheckCookie
                _httpLog = _errorPackage.AddNewHttpLog(nextUrl, _errorPackage.LastCheckPoint);
                if (!locationRedirect)
                {
                    CHECK(210);
                    nextUrl = LoginPhase3(nextUrl, false); // SetSid
                    _httpLog = _errorPackage.AddNewHttpLog(nextUrl, _errorPackage.LastCheckPoint);

                    // Setsid redirect?
                    if (nextUrl.Contains("SetSID"))
                    {
                        CHECK(291);
                        nextUrl = LoginPhase3(nextUrl, false); // SetSid again?
                        _httpLog = _errorPackage.AddNewHttpLog(nextUrl, _errorPackage.LastCheckPoint);
                    }

                    CHECK(211);
                    nextUrl = LoginPhase3(nextUrl, true); // Back to service login
                    nextUrl = ProxyTranslate(nextUrl);
                    _httpLog = _errorPackage.AddNewHttpLog(nextUrl, _errorPackage.LastCheckPoint);

                }
                CHECK(212);
                if (nextUrl.Contains("AccountRecoveryOptionsPrompt"))
                {
                    CHECK(292);
                    throw new AccountsRecoveryPromptException();
                }
                if (IsGoogleAppsAccount)
                {
                    // Two more redirects for apps normally. Some people get the GTLHS cookie already after this next phase
                    CHECK(293);
                    nextUrl = LoginPhase3(nextUrl, true); // Tasks/a/<domain> -> CheckCookies
                    CHECK(294);
                    if (!HaveAuthCookie()) nextUrl = LoginPhase3(nextUrl, true); // CheckCookies -> tasks/ig
                }

                CHECK(220);
                // 17. june - ANOTHER redirect needed? Just loop this maybe? 
                int chk = 240;
                for (int i = 0; i < 5 && !HaveAuthCookie(); i++)
                {
                    CHECK(chk++);
                    nextUrl = LoginPhase3(nextUrl, true); // CheckCookies -> tasks/ig
                    _httpLog = _errorPackage.AddNewHttpLog(nextUrl, _errorPackage.LastCheckPoint);
                }

                RemoveTempCookies();
                // Final call - Tasks! Auth in query string, will recieve GTL cookie upon return 
                if (!HaveAuthCookie())
                {
                    CHECK(221);
                    nextUrl = LoginPhase3(nextUrl, true); // Previously LoginPhase4(nextUrl); 
                    _httpLog = _errorPackage.AddNewHttpLog(nextUrl, _errorPackage.LastCheckPoint);
                }
                CHECK(213);
                if (!HaveAuthCookie()) throw new Exception("Did not receive Google authentication token after login procedure.");

                EnsureCorrectApiUrl();
            }
            Trace.WriteLine("Logging result: " + result);
            CHECK(999);
            _httpLog = null;
            return result;
        }

        private void EnsureCorrectApiUrl()
        { 
        }

        /// <summary>
        /// Returns true if we have the final auth cookie in our repository
        /// </summary>
        /// <returns></returns>
        private bool HaveAuthCookie()
        {
            foreach (var cookie in _cookies)
            {
                if (cookie.Name == AppsAuthCookie) return true;
                if (cookie.Name == GoogleAuthCookie) return true;
            }
            return false;
        }

        /// <summary>
        /// Returns true if the result of the login is so that we will be using the Google Apps URL
        /// (tasks/a/[domain]/ig instead of tasks/ig)
        /// </summary>
        /// <returns></returns>
        public bool WillBeUsingAppsUrl()
        {
            foreach (var cookie in _cookies)
            {
                if (cookie.Name == AppsAuthCookie) return true;
            }
            return false;
        }

        private string ProxyTranslate(string url)
        {
            if (GoogleApi.MailGoogleComHost.Equals(GoogleApi.MailGoogleComHostDefault)) return url; // No custom proxy

            // Replace host and possibly protocol
            string replaced = url.Replace(GoogleApi.MailGoogleComHostDefault, GoogleApi.MailGoogleComHost);
            if (GoogleApi.DoHttp) replaced = replaced.Replace("https:", "http:");
            
            return replaced;
        }

        private void CHECK(int point)
        {
            _errorPackage.AddCheckPoint(point);
        }

        /// <summary>
        /// Go to CheckCookie
        /// </summary>
        /// <param name="url"></param>
        private string LoginPhase2(string url, out bool locationRedirectSet)
        {
            CHECK(400);
            locationRedirectSet = false;
            var headers = new WebHeaderCollection();
            headers.Set("Cookie", _cookies.ToStringFor(url)); //"ISO-8859-1,utf-8;q=0.7,*;q=0.7"
            _httpLog.CookiesIn = _cookies.CloneShallow();
            CHECK(401);
            //headers.Set("Accept-Charset", "utf-8;q=0.7,*;q=0.7");
            var rq = GoogleApi.GetGetRequest(url, headers);
            _httpLog.SetHeadersIn(rq);

            CHECK(402);
            using (var response = rq.GetResponse())
            {
                CHECK(403);
                var newCookies = _cookies.SetCookiesFiltered(response);
                _httpLog.CookiesOut = newCookies;
                _httpLog.SetHeadersOut(response);
                CHECK(404);
                var sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
                CHECK(405);
                string html = sr.ReadToEnd();
                CHECK(406);
                _httpLog.SetHtml(html);
                var possibleRedirect = response.Headers["location"];
                if (possibleRedirect != null)
                {
                    Trace.WriteLine("Redirect received. Skipping SetSid");
                    CHECK(499);
                    locationRedirectSet = true;
                    return possibleRedirect;
                }
                string urlback = ParseUrlFromPhase2(html);
                CHECK(407);
                return urlback;
            }
        }

        /// <summary>
        /// Go to SetSID, but also general method for fetching a page and setting cookies from response 
        /// Returns next URL if location-header is set in response 
        /// </summary>
        /// <param name="url"></param>
        private string LoginPhase3(string url, bool withCookies)
        {
            if (url == null) throw new Exception("Did not receive redirect in login process when expected");
            CHECK(600);
            var headers = new WebHeaderCollection();
            if (withCookies)
            {
                headers.Set("Cookie", _cookies.ToStringFor(url)); //"ISO-8859-1,utf-8;q=0.7,*;q=0.7"
                _httpLog.CookiesIn = _cookies.CloneShallow();
            }

            CHECK(601);
            var rq = GoogleApi.GetGetRequest(url, headers);
            _httpLog.SetHeadersIn(rq);
            rq.KeepAlive = true;
            //rq.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
            //rq.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 6.1; nb-NO; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3";
            CHECK(602);
            using (var response = (HttpWebResponse)rq.GetResponse())
            {
                CHECK(603);
                var newCookies = _cookies.SetCookiesFiltered(response);
                _httpLog.CookiesOut = newCookies.CloneShallow();
                _httpLog.SetHeadersOut(response);
                // NYTT FOR LOG
                CHECK(612);
                var sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
                string html = sr.ReadToEnd();
                _httpLog.SetHtml(html);
                // END NYTT FOR LOG

                CHECK(604);
                if (response.StatusCode == HttpStatusCode.Redirect)
                {
                    CHECK(605);
                    string nextUrl = response.Headers["location"];
                    CHECK(606);
                    return nextUrl;
                }
                return null;
            }
        }

        private void RemoveTempCookies()
        {
            // TODO : fix properly removal of cookies , i.e. use expiration etc
            var remList = new List<Cookie>();
            foreach (Cookie c in _cookies)
            {
                if (c.Name != "SID" && c.Name != "HSID" && c.Name != AppsAuthCookie &&
                    c.Name != "NID" && c.Name != "SSID" && c.Name != GoogleAuthCookie) remList.Add(c); 
            }
            foreach (Cookie c in remList) _cookies.Remove(c);
        }

        /// <summary>
        /// Get GTL cookie from /tasks/ig
        /// </summary>
        /// <returns></returns>
        public void LoginPhase4(string url)
        {
            CHECK(700);
            // TODO : fix properly removal of cookies
            var remList = new List<Cookie>();
            bool appsCookieIsHere = false;
            foreach (Cookie c in _cookies)
            {
                if (c.Name != "SID" && c.Name != "HSID" && c.Name != "GTLHS" && 
                    c.Name != "NID" && c.Name != "SSID") remList.Add(c); // GTLHS = Google Apps Auth Cookie
                if (c.Name == "GTLHS") appsCookieIsHere = true;
            }
            CHECK(701);
            foreach (Cookie c in remList) _cookies.Remove(c);

            CHECK(702);
            if (!appsCookieIsHere)
            {
                var headers = new WebHeaderCollection();
                headers.Set("Cookie", _cookies.ToStringFor(url));
                _httpLog.CookiesIn = _cookies.CloneShallow();
                CHECK(703);
                var rq = GoogleApi.GetGetRequest(url, headers);
                CHECK(704);
                _httpLog.SetHeadersIn(rq);
                using (var response = rq.GetResponse())
                {

                    // NYTT FOR LOG
                    CHECK(799);
                    var sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
                    string html = sr.ReadToEnd();
                    _httpLog.SetHtml(html);
                    _httpLog.SetHeadersOut(response);
                    // END NYTT FOR LOG

                    CHECK(705);
                    var newCookies = _cookies.SetCookiesFiltered(response); // contains GTL
                    _httpLog.CookiesOut = newCookies.CloneShallow();
                    CHECK(707);
                    CheckGtlExists();
                }
            }
            CHECK(706);
        }

        private void CheckGtlExists()
        {
            foreach (Cookie cookie in _cookies)
            {
                if (cookie.Name == "GTL") return;
            }
            throw new GogTasksException("GTL Cookie not set");
        }

        /// <
        /// summary>
        /// Get GTL cookie from /tasks/ig
        /// </summary>
        /// <returns></returns>
        //public string GetMain()
        //{
        //    HttpWebRequest rq = (HttpWebRequest)WebRequest.Create(TasksUrl);
        //    rq.AllowAutoRedirect = false;
        //    rq.Method = "GET";
        //    rq.Headers.Set("Cookie", _cookies.ToString());
        //    var response = rq.GetResponse();
        //    _cookies.SetCookiesFiltered(response.Headers); // contains GTL
        //    var sr = new StreamReader(response.GetResponseStream());
        //    return sr.ReadToEnd();
        //}

        /// <summary>
        /// interprets the login html and returns specified type
        /// </summary>
        private LoginResult ParseLoginResult(HttpWebResponse response)
        {
            if (response.StatusCode == HttpStatusCode.Redirect)
            {
                return LoginResult.Ok;
            }
            var sr = new StreamReader(response.GetResponseStream());
            string html = sr.ReadToEnd();
            _httpLog.SetHtml(html);
            if (html.Contains("errormsg_0_Passwd")) return LoginResult.WrongUsernameOrPass;
            if (html.Contains("errormsg_0_logincaptcha")) return LoginResult.TuringTest;
            return LoginResult.Unknown;
        }

        public string ParseUrlFromPhase2(string html)
        {
            CHECK(500);
            string locRepStart = "url=&#39;";
            string locRepEnd = "\"></head>";


            int ix1 = html.IndexOf(locRepStart) + locRepStart.Length;
            int ix2 = html.IndexOf(locRepEnd, ix1 + 1);
            CHECK(501);

            if (ix1 == -1) throw new GogTasksException("ix1 zero in ParseUrlFromPhase2");
            if (ix2 == -1) throw new GogTasksException("ix2 zero in ParseUrlFromPhase2");
            string nexturl = html.Substring(ix1, ix2 - ix1);
            CHECK(502);
            return nexturl.Replace("&amp;", "&");
        }


        public void ParseLoginForm(string html)
        {
            XmlDocument doc = new XmlDocument();
            KeyVal = new Dictionary<string, string>();
            int ia = 0;
            int ib = 0;
            CHECK(111);
            while ((ia = html.IndexOf("<input", ia, StringComparison.InvariantCultureIgnoreCase)) != -1)
            {
                ib = html.IndexOf(">", ia);
                string tag = html.Substring(ia, ib - ia + 1);
                if (!tag.EndsWith("/>")) tag = tag.Substring(0, tag.Length - 1) + "/>";
                doc.LoadXml(tag);
                // Read name/value
                var nameNode = doc.DocumentElement.Attributes["name"];
                var valNode = doc.DocumentElement.Attributes["value"];
                if (nameNode == null) continue;
                string name = nameNode.Value;
                string value = valNode == null ? "" : valNode.Value;
                if (!KeyVal.ContainsKey(name)) KeyVal.Add(name, value);
                ia++;
            }
            CHECK(112);
        }
        
        /// <summary>
        /// Loads the login form
        /// </summary>
        public void LoadForm()
        {
            Trace.WriteLine("Fetching login form");
            string html = GetLoginForm();
            _httpLog.SetHtml(html);
            CHECK(110);
            ParseLoginForm(html);
            _httpLog = null;
            CHECK(113);
        }

        private string GetLoginForm()
        {
            string url = GoogleLoginFormUrl;
            if (IsGoogleAppsAccount) url = String.Format(AppsLoginFormUrl, GoogleAppsDomain);
            _httpLog = _errorPackage.AddNewHttpLog(url, 0);
            HttpWebRequest rq = (HttpWebRequest) WebRequest.Create(url);
            rq.AllowWriteStreamBuffering = AllowWriteStreamBuffering;
            CHECK(101);
            var response = rq.GetResponse();
            CHECK(102);
            var sr = new StreamReader(response.GetResponseStream());
            CHECK(103);
            string html = sr.ReadToEnd();
            sr.Close();
            CHECK(104);
            var newCookies=  _cookies.SetCookiesFiltered(response);
            if (_httpLog != null) _httpLog.CookiesOut = newCookies;
            CHECK(105);
            return html;
        }

    }



    /// <summary>
    /// A CertificatePolicy object that allows for untrusted certifcates
    /// </summary>
    public class TrustAllCertificatePolicy : System.Net.ICertificatePolicy
    {
        public TrustAllCertificatePolicy()
        { }

        public bool CheckValidationResult(ServicePoint sp,
         X509Certificate cert, WebRequest req, int problem)
        {
            //return problem == 0 || AppSettings.Settings.AllowUntrustedCerts;
            return true;
        }
    }

    public class GogTasksException : Exception
    {
        public GogTasksException(string message) : base(message)
        {
        }
    }

}
